シマーローディングエフェクトを作成する
アプリケーション開発では読み込み時間は避けられません。 ユーザーエクスペリエンス(UX)の観点から見ると、 最も重要なことはユーザーにそれを示すことです 読み込みが行われているということです。一般的なアプローチの 1 つ データがロードされていることをユーザーに伝えるには、 クロムカラーをきらめくアニメーションで表示します 読み込まれているコンテンツのタイプに近い形状。
次のアニメーションはアプリの動作を示しています。
このレシピは、コンテンツ ウィジェットを定義して配置することから始まります。 右下にはフローティング アクション ボタン (FAB) もあります。 ロードモードとロードモードを切り替えるコーナー これにより、実装を簡単に検証できます。
きらめく形を描く
この効果で光る形状は独立しています 最終的に読み込まれる実際のコンテンツから。
したがって、目標は、次のような形状を表示することです。 最終的な内容をできるだけ正確に。
正確な形状を表示するのは簡単です。 コンテンツには明確な境界があります。たとえば、このレシピでは、 いくつかの円形の画像といくつかの角丸長方形の画像があります。 輪郭に正確に一致する形状を描くことができます それらの画像の。
一方、その下に表示されるテキストについて考えてみましょう。 角丸長方形の画像。何行あるかわかりません テキストは読み込まれるまで存在します。 したがって、長方形を描こうとしても意味がありません。 テキストの各行に対して。代わりに、データのロード中に、 非常に薄い角丸長方形をいくつか描きます。 表示されるテキストを表します。形と大きさ 完全には一致しませんが、問題ありません。
画面上部の循環リスト項目から始めます。
それぞれを確認してくださいCircleListItem
ウィジェットには円が表示されます
画像の読み込み中に色が付きます。
class CircleListItem extends StatelessWidget {
const CircleListItem({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Container(
width: 54,
height: 54,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
child: ClipOval(
child: Image.network(
'https://flutter'
'.dev/docs/cookbook/img-files/effects/split-check/Avatar1.jpg',
fit: BoxFit.cover,
),
),
),
);
}
}
ウィジェットが何らかの形状を表示している限り、 このレシピではシマー効果を適用できます。
に似ていますCircleListItem
ウィジェット、
確実にCardListItem
ウィジェット
画像が表示される場所に色を表示します。
また、CardListItem
ウィジェット、
テキストの表示と
現在の読み込みステータスに基づいて四角形が表示されます。
class CardListItem extends StatelessWidget {
const CardListItem({
super.key,
required this.isLoading,
});
final bool isLoading;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImage(),
const SizedBox(height: 16),
_buildText(),
],
),
);
}
Widget _buildImage() {
return AspectRatio(
aspectRatio: 16 / 9,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
'https://flutter'
'.dev/docs/cookbook/img-files/effects/split-check/Food1.jpg',
fit: BoxFit.cover,
),
),
),
);
}
Widget _buildText() {
if (isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 24,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(height: 16),
Container(
width: 250,
height: 24,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
),
],
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do '
'eiusmod tempor incididunt ut labore et dolore magna aliqua.',
),
);
}
}
}
UI は、以下に応じて異なる方法でレンダリングされるようになりました。 ロード中かロード中か。 画像URLを一時的にコメントアウトすることで、 UI が 2 つの方法でレンダリングされることがわかります。
次の目標は、色付きの領域をすべてペイントすることです。 きらめきのように見える単一のグラデーション。
シマーグラデーションをペイントする
このレシピで達成される効果の鍵は、ウィジェットを使用することです
呼ばれたShaderMask
。のShaderMask
ウィジェットはその名の通り、
シェーダをその子に適用しますが、その領域にのみ適用します。
子供はすでに何かを描いていました。例えば、
シェーダを適用するのは黒いシェイプのみです。
前に設定しました。
に適用されるクロム色の線形グラデーションを定義します。 きらめく形。
const _shimmerGradient = LinearGradient(
colors: [
Color(0xFFEBEBF4),
Color(0xFFF4F4F4),
Color(0xFFEBEBF4),
],
stops: [
0.1,
0.3,
0.4,
],
begin: Alignment(-1.0, -0.3),
end: Alignment(1.0, 0.3),
tileMode: TileMode.clamp,
);
という新しいステートフル ウィジェットを定義します。ShimmerLoading
与えられたものをラップするchild
ウィジェット付きShaderMask
。
を設定します。ShaderMask
シマーを適用するウィジェット
シェーダとしてのグラデーションblendMode
のsrcATop
。
のsrcATop
ブレンド モードは、任意の色を置き換えます。child
シェーダーカラーでペイントされたウィジェット。
class ShimmerLoading extends StatefulWidget {
const ShimmerLoading({
super.key,
required this.isLoading,
required this.child,
});
final bool isLoading;
final Widget child;
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading> {
@override
Widget build(BuildContext context) {
if (!widget.isLoading) {
return widget.child;
}
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return _shimmerGradient.createShader(bounds);
},
child: widget.child,
);
}
}
を包みますCircleListItem
ウィジェットShimmerLoading
ウィジェット。
Widget _buildTopRowItem() {
return ShimmerLoading(
isLoading: _isLoading,
child: const CircleListItem(),
);
}
を包みますCardListItem
ウィジェットShimmerLoading
ウィジェット。
Widget _buildListItem() {
return ShimmerLoading(
isLoading: _isLoading,
child: CardListItem(
isLoading: _isLoading,
),
);
}
図形の読み込み中に、表示されるようになりました
シマーグラデーション
から戻ってきましたshaderCallback
。
これは正しい方向への大きな一歩です
しかし、このグラデーション表示には問題があります。
各CircleListItem
ウィジェットとそれぞれのCardListItem
ウィジェット
新しいバージョンのグラデーションを表示します。
このレシピでは、画面全体が
一つの大きなきらめく表面のように見えます。
この問題は次のステップで解決します。
大きなきらめきを 1 つペイントする
画面全体に大きなきらめきをペイントするには、
各ShimmerLoading
ウィジェットのニーズ
同じ全画面グラデーションベースのペイント
その位置についてShimmerLoading
画面上のウィジェット。
より正確に言えば、シマーであると仮定するのではなく、
画面全体を占める必要があります。
輝きを共有する領域がいくつかあるはずです。
おそらくその領域が画面全体を占めるでしょう、
あるいはそうではないかもしれません。これを解決する方法は
Flutter における一種の問題は、別のウィジェットを定義することです
すべての上に位置するものShimmerLoading
ウィジェット
ウィジェットツリー内でそれを呼び出しますShimmer
。
それでは、それぞれShimmerLoading
ウィジェットが参照を取得する
にShimmer
祖先
そして、表示する希望のサイズとグラデーションを要求します。
という新しいステートフル ウィジェットを定義します。Shimmer
それか
を取り込みますLinearGradient
そして子孫を提供します
にアクセスできるState
物体。
class Shimmer extends StatefulWidget {
static ShimmerState? of(BuildContext context) {
return context.findAncestorStateOfType<ShimmerState>();
}
const Shimmer({
super.key,
required this.linearGradient,
this.child,
});
final LinearGradient linearGradient;
final Widget? child;
@override
ShimmerState createState() => ShimmerState();
}
class ShimmerState extends State<Shimmer> {
@override
Widget build(BuildContext context) {
return widget.child ?? const SizedBox();
}
}
にメソッドを追加します。ShimmerState
順番にクラス
へのアクセスを提供するため、linearGradient
、
のサイズShimmerState
のRenderBox
、
そして、その中の子孫の位置を調べます。ShimmerState
のRenderBox
。
class ShimmerState extends State<Shimmer> {
Gradient get gradient => LinearGradient(
colors: widget.linearGradient.colors,
stops: widget.linearGradient.stops,
begin: widget.linearGradient.begin,
end: widget.linearGradient.end,
);
bool get isSized => (context.findRenderObject() as RenderBox?)?.hasSize ?? false;
Size get size => (context.findRenderObject() as RenderBox).size;
Offset getDescendantOffset({
required RenderBox descendant,
Offset offset = Offset.zero,
}) {
final shimmerBox = context.findRenderObject() as RenderBox;
return descendant.localToGlobal(offset, ancestor: shimmerBox);
}
@override
Widget build(BuildContext context) {
return widget.child ?? const SizedBox();
}
}
画面のすべてのコンテンツをShimmer
ウィジェット。
class _ExampleUiLoadingAnimationState extends State<ExampleUiLoadingAnimation> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Shimmer(
linearGradient: _shimmerGradient,
child: ListView(
// ListView Contents
),
),
);
}
}
使用Shimmer
ウィジェット内のShimmerLoading
共有グラデーションをペイントするウィジェット。
class _ShimmerLoadingState extends State<ShimmerLoading> {
@override
Widget build(BuildContext context) {
if (!widget.isLoading) {
return widget.child;
}
// Collect ancestor shimmer information.
final shimmer = Shimmer.of(context)!;
if (!shimmer.isSized) {
// The ancestor Shimmer widget isn’t laid
// out yet. Return an empty box.
return const SizedBox();
}
final shimmerSize = shimmer.size;
final gradient = shimmer.gradient;
final offsetWithinShimmer = shimmer.getDescendantOffset(
descendant: context.findRenderObject() as RenderBox,
);
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
return gradient.createShader(
Rect.fromLTWH(
-offsetWithinShimmer.dx,
-offsetWithinShimmer.dy,
shimmerSize.width,
shimmerSize.height,
),
);
},
child: widget.child,
);
}
}
あなたのShimmerLoading
ウィジェットに共有されたものが表示されるようになりました
内のすべてのスペースを占めるグラデーションShimmer
ウィジェット。
きらめきをアニメーション化する
シマーグラデーションを動かすには、 きらめく輝きを与えます。
のLinearGradient
というプロパティがありますtransform
グラデーションの外観を変えるために使用できます。
たとえば、水平方向に移動します。
のtransform
プロパティはGradientTransform
実例。
というクラスを定義します_SlidingGradientTransform
実装するGradientTransform
横方向にスライドする外観を実現します。
class _SlidingGradientTransform extends GradientTransform {
const _SlidingGradientTransform({
required this.slidePercent,
});
final double slidePercent;
@override
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0);
}
}
グラデーション スライドのパーセンテージは時間の経過とともに変化します
動きのある外観を作成するため。
パーセンテージを変更するには、AnimationController
の中にShimmerState
クラス。
class ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
late AnimationController _shimmerController;
@override
void initState() {
super.initState();
_shimmerController = AnimationController.unbounded(vsync: this)
..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000));
}
@override
void dispose() {
_shimmerController.dispose();
super.dispose();
}
}
を適用します。_SlidingGradientTransform
にgradient
を使用して_shimmerController
のvalue
としてslidePercent
。
LinearGradient get gradient => LinearGradient(
colors: widget.linearGradient.colors,
stops: widget.linearGradient.stops,
begin: widget.linearGradient.begin,
end: widget.linearGradient.end,
transform:
_SlidingGradientTransform(slidePercent: _shimmerController.value),
);
グラデーションがアニメーション化されますが、個人のShimmerLoading
ウィジェット自体は再描画されません
グラデーションが変化するので。したがって、何もないように見えます
起こっている。
を公開します_shimmerController
からShimmerState
としてListenable
。
Listenable get shimmerChanges => _shimmerController;
のShimmerLoading
、祖先への変更をリッスンします。ShimmerState
のshimmerChanges
財産、
そしてシマーグラデーションを再ペイントします。
class _ShimmerLoadingState extends State<ShimmerLoading> {
Listenable? _shimmerChanges;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_shimmerChanges != null) {
_shimmerChanges!.removeListener(_onShimmerChange);
}
_shimmerChanges = Shimmer.of(context)?.shimmerChanges;
if (_shimmerChanges != null) {
_shimmerChanges!.addListener(_onShimmerChange);
}
}
@override
void dispose() {
_shimmerChanges?.removeListener(_onShimmerChange);
super.dispose();
}
void _onShimmerChange() {
if (widget.isLoading) {
setState(() {
// update the shimmer painting.
});
}
}
}
おめでとう! これで全画面表示になりました。 回転するアニメーションのシマーエフェクト コンテンツの読み込みに応じてオンとオフが切り替わります。